package com.fowo.api.sys.service.impl;

import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.net.NetUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fowo.api.common.model.RException;
import com.fowo.api.common.util.ListUtils;
import com.fowo.api.config.AppProperties;
import com.fowo.api.sys.entity.*;
import com.fowo.api.sys.entity.enums.SysTaskType;
import com.fowo.api.sys.mapper.SysTaskMapper;
import com.fowo.api.sys.model.SysTaskData;
import com.fowo.api.sys.model.SysTaskErrorMessageTemplateData;
import com.fowo.api.sys.service.*;
import com.fowo.api.user.model.JwtUserInfo;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.openxml4j.exceptions.InvalidOperationException;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.support.CronExpression;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import javax.sql.DataSource;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.*;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;

@Slf4j(topic = "系统任务执行服务")
@Service
public class SysTaskExecutionServiceImpl implements SysTaskExecutionService, ApplicationContextAware {
    private static final String TO_USER_PARAM_KEY = "sysTask.failNotify.userId";
    private static final Duration MAX_TASK_EXEC_TIME = Duration.ofMinutes(60);

    @Resource
    private AppProperties appProperties;
    @Resource
    private SysTaskMapper sysTaskMapper;
    @Resource
    private SysTaskLogService sysTaskLogService;
    @Resource
    private SysTaskDataSourceService sysTaskDataSourceService;
    @Resource
    private SysParamService sysParamService;
    @Resource
    private SysMessageService sysMessageService;
    @Resource
    private SysMailService sysMailService;
    @Resource
    private RedisTemplate<String, Object> redisTemplate;
    @Resource
    private ObjectMapper objectMapper;

    private ApplicationContext applicationContext;
    private ThreadPoolTaskScheduler taskScheduler;
    private Map<Long, SysTaskExecution> taskExecutions = new ConcurrentHashMap<>();

    private void initTaskScheduler() {
        log.info("初始化线程池");
        ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler();
        threadPoolTaskScheduler.setPoolSize(20);
        threadPoolTaskScheduler.setThreadNamePrefix("sysTaskExecutor-");
        threadPoolTaskScheduler.setWaitForTasksToCompleteOnShutdown(false);
        threadPoolTaskScheduler.setAwaitTerminationSeconds(60);
        threadPoolTaskScheduler.setRemoveOnCancelPolicy(true);
        threadPoolTaskScheduler.initialize();
        this.taskScheduler = threadPoolTaskScheduler;
        // 启动 5 秒后再加载全部系统任务
        this.taskScheduler.schedule(this::preload, Instant.now().plusSeconds(5));
    }

    private void preload() {
        log.info("检查上次关闭时未正确结束的任务");
        List<SysTask> needEndTasks = sysTaskMapper.selectList(Wrappers.<SysTask>query()
                .eq("plan_worker_id", appProperties.getWorkerId())
                .eq("status", 1)
                .select("id", "last_execution_sync_id")
        );
        for (SysTask needEndTask : needEndTasks) {
            // 解同步锁
            if (StringUtils.hasText(needEndTask.getLastExecutionSyncId())) {
                redisUnlockTask(needEndTask.getId(), needEndTask.getLastExecutionSyncId());
            }
            // 配置任务停止
            sysTaskMapper.updateEndExecutions(needEndTask.getId());
        }

        log.info("加载所有系统任务");
        List<SysTask> sysTasks = sysTaskMapper.selectAll();
        for(SysTask sysTask : sysTasks) {
            loadTask(sysTask);
        }
    }

    private SysTaskExecution getTaskExecution(Long id, boolean reload) {
        synchronized (taskExecutions) {
            if(taskExecutions.containsKey(id)) {
                SysTaskExecution sysTaskExecution = taskExecutions.get(id);
                if (reload) {
                    loadTask(id);
                }
                return sysTaskExecution;
            }
            loadTask(id);
            return taskExecutions.get(id);
        }
    }

    private void loadTask(Long id) {
        SysTask sysTask = sysTaskMapper.selectById(id);
        if (sysTask != null) {
            loadTask(sysTask);
        } else if(taskExecutions.containsKey(id)) {
            // 任务已不存在，取消计划、停止已开始的任务
            SysTaskExecution sysTaskExecution = taskExecutions.get(id);
            sysTaskExecution.isDeleted = true;
            sysTaskExecution.cancelSchedule(false);
            sysTaskExecution.stop();
            taskExecutions.remove(id);
        }
    }

    private void loadTask(SysTask sysTask) {
        SysTaskExecution sysTaskExecution = taskExecutions.get(sysTask.getId());
        if (sysTaskExecution != null) {
            sysTaskExecution.setSysTask(sysTask);
            sysTaskExecution.scheduleNext();
            return;
        }

        sysTaskExecution = new SysTaskExecution(sysTask);
        sysTaskExecution.scheduleNext();
        taskExecutions.put(sysTask.getId(), sysTaskExecution);
    }

    @Override
    public boolean start(String operator, Long id) {
        if (this.taskScheduler == null) {
            throw new RException("线程池未就绪，请稍后再试");
        }
        sysTaskLogService.create(id, SysTaskLog.STATUS_NONE, String.format("%s手动执行任务", operator));
        SysTaskExecution sysTaskExecution = getTaskExecution(id, false);
        if(sysTaskExecution != null) {
            sysTaskExecution.scheduledFuture = this.taskScheduler.schedule(sysTaskExecution, Instant.now());
            return true;
        }
        return false;
    }

    @Override
    public boolean stop(String operator, Long id) {
        SysTaskExecution sysTaskExecution = getTaskExecution(id, false);
        if(sysTaskExecution != null) {
            sysTaskExecution.stop();
            sysTaskExecution.cancelSchedule(true);
            sysTaskExecution.join();
            touch(id);
        }
        return false;
    }

    @Override
    public void touch(Long id) {
        touch(id, 1);
    }

    private void touch(Long id, int waitSeconds) {
        this.taskScheduler.schedule(
                () -> reloadTask(id),
                Instant.now().plusSeconds(waitSeconds)
        );
    }

    private void reloadTask(Long id) {
        getTaskExecution(id, true);
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
        initTaskScheduler();
    }

    public static final String REDIS_TASK_LOCK_KEY_PREFIX = "REDIS_TASK_LOCK.";

    private boolean redisLockTask(Long taskId, String taskTraceId) {
        String lockKey = REDIS_TASK_LOCK_KEY_PREFIX + taskId.toString();
        return Boolean.TRUE.equals(redisTemplate.opsForValue().setIfAbsent(lockKey, taskTraceId, MAX_TASK_EXEC_TIME));
    }

    private void redisUnlockTask(Long taskId, String taskTraceId) {
        String lockKey = REDIS_TASK_LOCK_KEY_PREFIX + taskId.toString();
        String lockValue = (String) redisTemplate.opsForValue().get(lockKey);
        if (Objects.equals(lockValue, taskTraceId)) {
            redisTemplate.delete(lockKey);
        }
    }

    @Data
    class SysTaskExecution implements Runnable {
        // 任务是否已删除
        public boolean isDeleted;
        // 系统任务
        private SysTask sysTask;
        // 停止信号
        private boolean stopSignal;
        // 运行锁
        private Object mLock;
        // 计划状态
        private ScheduledFuture<?> scheduledFuture;
        // 重试计划状态
        private ScheduledFuture<?> retryScheduledFuture;
        // 重试次数
        private int retryTimes;

        public SysTaskExecution(SysTask sysTask) {
            this.sysTask = sysTask;
        }

        private void raiseIfCancellation() {
            if (stopSignal || isDeleted) {
                throw new CancellationException();
            }
        }

        @Override
        public void run() {
            Environment env = SpringUtil.getBean(ConfigurableEnvironment.class);
            String runNode = StrUtil.concat(true, NetUtil.getLocalhostStr(), ":", env.getProperty("spring.servlet.port"));
            String taskTraceId = UUID.randomUUID().toString();
            if (!redisLockTask(getSysTask().getId(), taskTraceId)) {
                // 已有其它云节点在进行此任务，忽略此次执行，等 5 秒更新一次配置
                touch(getSysTask().getId(), 5);
                return;
            }

            mLock = new Object();
            try {
                sysTaskMapper.updateStartExecutions(sysTask.getId(), appProperties.getWorkerId(), taskTraceId);
                sysTaskLogService.create(sysTask.getId(), SysTaskLog.STATUS_RUNNING, "开始执行，执行节点：" + runNode);
                final SysTaskData data = sysTask.getData();
                if (data == null) {
                    throw new Exception("配置不正常，任务配置数据为空");
                }
                try {
                    if (sysTask.getType() == SysTaskType.Sql) {
                        runSql();
                    } else if (sysTask.getType() == SysTaskType.Api) {
                        runApi();
                    } else if (sysTask.getType() == SysTaskType.Inner) {
                        runInner();
                    } else {
                        throw new InvalidOperationException(String.format("%s 不是已知的任务类型", sysTask.getType()));
                    }
                } catch (CancellationException ce) {
                    log.info("{}任务被取消！", sysTask.getTitle());
                    sysTaskLogService.create(sysTask.getId(), SysTaskLog.STATUS_NONE, "执行被手工取消");
                } catch (Exception e) {
                    log.error(String.format("%s任务执行发生错误！", sysTask.getTitle()), e);
                    sysTaskLogService.create(sysTask.getId(), SysTaskLog.STATUS_ERROR,  StrUtil.subPre(ExceptionUtil.getRootCauseMessage(e),2000) );
                    if (data.getRetryTimes() != null && data.getRetryTimes() > 0 && retryTimes < data.getRetryTimes()) {
                        int retryInterval = data.getRetryInterval() == null ? 1 : data.getRetryInterval();
                        // 重试
                        this.retryTimes++;
                        this.retryScheduledFuture = taskScheduler.schedule(this, Instant.now().plusSeconds(retryInterval));
                    } else if (data.getFailNotify() != null && data.getFailNotify().size() > 0) {
                        SysUser toUser = sysParamService.getUserByName(TO_USER_PARAM_KEY, null);
                        if (toUser == null) {
                            sysTaskLogService.create(sysTask.getId(), SysTaskLog.STATUS_ERROR, "无法发送错误通知，未配置系统默认");
                            return;
                        }
                        // 错误通知处理
                        if (data.getFailNotify().contains("message")) {
                            // 站内消息
                            sysMessageService.createSysMessage(
                                    0L,
                                    SysMessage.STATUS_URGENT,
                                    "系统任务运行失败",
                                    String.format("任务 %s 运行失败\n%s", sysTask.getTitle(), e.toString()),
                                    String.format("/system/task/log/%d", sysTask.getId())
                            );
                        }
                        if (data.getFailNotify().contains("email")) {
                            SysTaskErrorMessageTemplateData tpl = new SysTaskErrorMessageTemplateData();
                            tpl.setCreateTime(new Date());
                            tpl.setId(sysTask.getId());
                            tpl.setTaskName(sysTask.getTitle());
                            tpl.setMessage(String.format("任务 %s 运行失败\n%s", sysTask.getTitle(), e.toString()));
                            sysMailService.sendTaskError(toUser.getVerifiedMail(), tpl);
                        }
                    }
                }
            }
            catch (Exception e) {

            }
            finally {
                sysTaskMapper.updateEndExecutions(sysTask.getId());
                redisUnlockTask(sysTask.getId(), taskTraceId);
                sysTaskLogService.create(sysTask.getId(), SysTaskLog.STATUS_NONE, "结束执行，执行节点：" + runNode);
                scheduleNext();
                synchronized (mLock) {
                    mLock.notifyAll();
                }
            }
        }

        private void runApi() throws Exception {

        }

        private Object getInnerClassInstance() {
            try {
                Class<?> runClass = Class.forName(sysTask.getData().getClassName());
                if (runClass.isInterface() || runClass.isAnnotationPresent(Service.class) || runClass.isAnnotationPresent(Component.class)) {
                    // 通过 Spring Bean 模式来获取实例
                    return applicationContext.getBean(runClass);
                }
                // 创建新实例
                return runClass.getConstructor().newInstance();
            }
            catch(Exception ex) {
                throw new InvalidOperationException(String.format("无法实例化任务操作类：%s", sysTask.getData().getClassName()), ex);
            }
        }

        private void runInner() throws Exception {
            Object runInstance = getInnerClassInstance();
            Method method = ListUtils.find(Arrays.asList(runInstance.getClass().getMethods().clone()), m -> m.getName().equals(sysTask.getData().getMethod()));
            //构造来宾用户权限
            JwtUserInfo.setGuestCurrent();
            if (method.getParameterCount() > 0) {
                // 需要解析一个参数
                Object paramter = objectMapper.readValue(sysTask.getData().getParamsJson(), method.getParameterTypes()[0]);
                method.invoke(runInstance, paramter);
            } else {
                method.invoke(runInstance);
            }
        }

        private void runSql() throws Exception {
            raiseIfCancellation();
            DataSource dataSource = getDataSource();
            if (dataSource == null) {
                throw new Exception("数据源配置无效");
            }
            raiseIfCancellation();
            try(Connection connection = dataSource.getConnection()) {
                sysTaskLogService.create(sysTask.getId(), SysTaskLog.STATUS_NONE, "数据库已连接");
                PreparedStatement preparedStatement = connection.prepareStatement(sysTask.getData().getSql());
                preparedStatement.execute();
                sysTaskLogService.create(sysTask.getId(), SysTaskLog.STATUS_NONE, "SQL 执行完成");
            }
        }

        public void stop() {
            stopSignal = true;
        }

        public void join() {
            if (mLock != null) {
                try {
                    synchronized (mLock) {
                        mLock.wait();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

        /**
         * 计划下一个执行时间
         */
        public void scheduleNext() {
            if (isDeleted) {
                this.cancelSchedule(false);
                return;
            }
            if (sysTask.getPlan() == null || !sysTask.getPlan()) {
                // 未启用的计划
                this.cancelSchedule(false);
                this.scheduledFuture = null;
                return;
            }
            this.cancelSchedule(false);
            if (sysTask.getData() != null &&
                    "cron".equals(sysTask.getData().getLoopType())) {
                try {
                    CronExpression cronExpression = CronExpression.parse(sysTask.getData().getCron());
                    LocalDateTime next = cronExpression.next(LocalDateTime.now());
                    if (next != null) {

                        // 更新下次执行时间
                        final Instant instant = next.atZone(ZoneId.systemDefault()).toInstant();
                        Date nextExecutionTime = Date.from(instant);
                        SysTask updateSysTask = new SysTask();
                        updateSysTask.setId(sysTask.getId());
                        updateSysTask.setNextExecutionTime(nextExecutionTime);
                        sysTaskMapper.updateById(updateSysTask);
                        // 计划下次执行
                        this.scheduledFuture = taskScheduler.schedule(this, instant);
                        sysTaskLogService.create(sysTask.getId(), SysTaskLog.STATUS_NONE, String.format("计划下次在 %s 执行", instant.toString()));
                    }
                } catch (Exception e) {
                    String message = String.format("%s任务计划执行失败", sysTask.getTitle());
                    log.error(message, e);
                    sysTaskLogService.create(sysTask.getId(), SysTaskLog.STATUS_ERROR, message + "\n" + e.toString());
                }
            } else {
                this.scheduledFuture = null;
            }
        }

        /**
         * 取消下一个执行计划
         */
        public void cancelSchedule(boolean force) {
            if(this.retryScheduledFuture != null && !this.retryScheduledFuture.isCancelled()) {
                retryScheduledFuture.cancel(force);
            }
            if(this.scheduledFuture != null && !this.scheduledFuture.isCancelled()) {
                scheduledFuture.cancel(force);
            }
        }

        private DataSource getDataSource() {
            if (sysTask.getData().getDataSourceId() == null || sysTask.getData().getDataSourceId() == 0) {
                return applicationContext.getBean(DataSource.class);
            }
            final SysTaskDataSource taskDataSource = sysTaskDataSourceService.getById(sysTask.getData().getDataSourceId());
            if (taskDataSource != null) {
                DriverManagerDataSource dataSource = new DriverManagerDataSource();
                dataSource.setDriverClassName(taskDataSource.getDriver());
                dataSource.setUrl(taskDataSource.getUrl());
                dataSource.setUsername(taskDataSource.getUsername());
                dataSource.setPassword(taskDataSource.getPassword());
                return dataSource;
            }
            return null;
        }
    }
}
